 /*******************************************************************************
  * Copyright (c) 2000, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.update.core.model;

 import java.lang.reflect.Array ;
 import java.net.MalformedURLException ;
 import java.net.URL ;
 import java.net.URLClassLoader ;
 import java.util.HashMap ;
 import java.util.Iterator ;
 import java.util.List ;
 import java.util.Locale ;
 import java.util.Map ;
 import java.util.MissingResourceException ;
 import java.util.ResourceBundle ;
 import java.util.Set ;

 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.Path;
 import org.eclipse.core.runtime.PlatformObject;
 import org.eclipse.update.core.Feature;
 import org.eclipse.update.core.SiteManager;
 import org.eclipse.update.internal.core.Messages;
 import org.eclipse.update.internal.core.UpdateCore;
 import org.eclipse.update.internal.core.UpdateManagerUtils;

 /**
  * Root model object. Extended by all model objects.
  * <p>
  * This class cannot be instantiated and must be subclassed.
  * </p>
  * <p>
  * <b>Note:</b> This class/interface is part of an interim API that is still under development and expected to
  * change significantly before reaching stability. It is being made available at this early stage to solicit feedback
  * from pioneering adopters on the understanding that any code that uses this API will almost certainly be broken
  * (repeatedly) as the API evolves.
  * </p>
  */
 public abstract class ModelObject extends PlatformObject {

     private boolean readOnly = false;

     private static final String KEY_PREFIX = "%"; //$NON-NLS-1$
 private static final String KEY_DOUBLE_PREFIX = KEY_PREFIX + KEY_PREFIX;

     private static Map bundles;

     /**
      * Creates a base model object.
      *
      * @since 2.0
      */
     protected ModelObject() {
     }

     /**
      * Checks that this model object is writeable. A runtime exception
      * is thrown if it is not.
      *
      * @since 2.0
      */
     protected final void assertIsWriteable() {
         Assert.isTrue(!isReadOnly(), Messages.ModelObject_ModelReadOnly);
     }

     /**
      * Sets this model object and all of its descendents to be read-only.
      * Subclasses may extend this implementation.
      *
      * @see #isReadOnly
      * @since 2.0
      */
     public void markReadOnly() {
         readOnly = true;
     }

     /**
      * Returns whether or not this model object is read-only.
      *
      * @return <code>true</code> if this model object is read-only,
      * <code>false</code> otherwise
      * @see #markReadOnly
      * @since 2.0
      */
     public boolean isReadOnly() {
         return readOnly;
     }

     /**
      * Delegate setting of read-only
      *
      * @param o object to delegate to. Must be of type ModelObject.
      * @see #isReadOnly
      * @since 2.0
      */
     protected void markReferenceReadOnly(ModelObject o) {
         if (o == null)
             return;
         o.markReadOnly();
     }

     /**
      * Delegate setting of read-only
      *
      * @param o object array to delegate to. Each element must be of type ModelObject.
      * @see #isReadOnly
      * @since 2.0
      */
     protected void markListReferenceReadOnly(ModelObject[] o) {
         if (o == null)
             return;
         for (int i = 0; i < o.length; i++) {
             o[i].markReadOnly();
         }
     }

     /**
      * Resolve the model element. This method allows any relative URL strings
      * to be resolved to actual URL. It also allows any translatable strings
      * to be localized.
      *
      * Subclasses need to override this method to perform the actual resolution.
      * @param base base URL.
      * @param bundleURL resource bundle URL.
      * @exception MalformedURLException
      * @since 2.0
      */
     public void resolve(URL base, URL bundleURL) throws MalformedURLException {
         return;
     }

     /**
      * Delegate resolution to referenced model
      *
      * @param o object to delegate to. Must be of type ModelObject.
      * @param url base URL.
      * @param bundleURL resource bundle URL.
      * @exception MalformedURLException
      * @since 2.0
      */
     protected void resolveReference(ModelObject o, URL url, URL bundleURL) throws MalformedURLException {
         if (o == null)
             return;
         o.resolve(url, bundleURL);
     }

     /**
      * Delegate resolution to list of referenced models
      *
      * @param o object array to delegate to. Each element must be of type ModelObject.
      * @param url base URL.
      * @param bundleURL resource bundle URL.
      * @exception MalformedURLException
      * @since 2.0
      */
     protected void resolveListReference(ModelObject[] o, URL url, URL bundleURL) throws MalformedURLException {
         if (o == null)
             return;
         for (int i = 0; i < o.length; i++) {
             o[i].resolve(url, bundleURL);
         }
     }

     /**
      * Resolve a URL based on context
      *
      * @param context base URL.
      * @param bundleURL resource bundle URL.
      * @param urlString url string from model.
      * @return URL, or <code>null</code>.
      * @exception MalformedURLException
      * @since 2.0
      */
     protected URL resolveURL(URL context, URL bundleURL, String urlString) throws MalformedURLException {

         // URL string was not specified
 if (urlString == null || urlString.trim().equals("")) //$NON-NLS-1$
 return null;

         // check to see if we have NL-sensitive URL
 String resolvedUrlString = resolveNLString(bundleURL, urlString);

         resolvedUrlString = resolvePlatfromConfiguration(resolvedUrlString);

         // if we don't have a base url, use only the supplied string
 if (context == null)
             return new URL (resolvedUrlString);

         // otherwise return new URL in context of base URL
 return new URL (context, resolvedUrlString);
     }
     /**
      * Resolves the URL based on platfrom Configuration
      * $os$\$ws$\license.txt will become
      * win32\win32\license.txt on a system where os=win32 and ws=win32
      *
      * @param resolvedUrlString
      * @return String
      */
     private String resolvePlatfromConfiguration(String resolvedUrlString) {
         int osIndex = resolvedUrlString.indexOf("$os$"); //$NON-NLS-1$
 if (osIndex != -1)
             return getExtendedString(resolvedUrlString);

         int wsIndex = resolvedUrlString.indexOf("$ws$"); //$NON-NLS-1$
 if (wsIndex != -1)
             return getExtendedString(resolvedUrlString);

         int nlIndex = resolvedUrlString.indexOf("$nl$"); //$NON-NLS-1$
 if (nlIndex != -1)
             return getExtendedString(resolvedUrlString);

         int archIndex = resolvedUrlString.indexOf("$arch$"); //$NON-NLS-1$
 if (archIndex != -1)
             return getExtendedString(resolvedUrlString);

         return resolvedUrlString;
     }

     private String getExtendedString(String resolvedUrlString) {
         IPath path = new Path(resolvedUrlString);
         path = getExpandedPath(path);
         if (UpdateCore.DEBUG && UpdateCore.DEBUG_SHOW_WARNINGS) {
             UpdateCore.warn("Resolved :" + resolvedUrlString + " as:" + path.toOSString()); //$NON-NLS-1$ //$NON-NLS-2$
 }

         return path.toOSString();
     }

     private IPath getExpandedPath(IPath path) {
         String first = path.segment(0);
         if (first != null) {
             IPath rest = getExpandedPath(path.removeFirstSegments(1));
             if (first.equals("$ws$")) { //$NON-NLS-1$
 path = new Path(SiteManager.getWS()).append(rest);
             } else if (first.equals("$os$")) { //$NON-NLS-1$
 path = new Path(SiteManager.getOS()).append(rest);
             } else if (first.equals("$nl$")) { //$NON-NLS-1$
 path = new Path(SiteManager.getNL()).append(rest);
             } else if (first.equals("$arch$")) { //$NON-NLS-1$
 path = new Path(SiteManager.getOSArch()).append(rest);
             }
         }
         return path;
     }

     /**
      * Returns a resource string corresponding to the given argument
      * value and bundle.
      * If the argument value specifies a resource key, the string
      * is looked up in the given resource bundle. If the argument does not
      * specify a valid key, the argument itself is returned as the
      * resource string. The key lookup is performed against the
      * specified resource bundle. If a resource string
      * corresponding to the key is not found in the resource bundle
      * the key value, or any default text following the key in the
      * argument value is returned as the resource string.
      * A key is identified as a string begining with the "%" character.
      * Note that the "%" character is stripped off prior to lookup
      * in the resource bundle.
      * <p>
      * For example, assume resource bundle plugin.properties contains
      * name = Project Name
      * <pre>
      * resolveNLString(b,"Hello World") returns "Hello World"</li>
      * resolveNLString(b,"%name") returns "Project Name"</li>
      * resolveNLString(b,"%name Hello World") returns "Project Name"</li>
      * resolveNLString(b,"%abcd Hello World") returns "Hello World"</li>
      * resolveNLString(b,"%abcd") returns "%abcd"</li>
      * resolveNLString(b,"%%name") returns "%name"</li>
      * </pre>
      * </p>
      *
      * @param bundleURL resource bundle url.
      * @param string translatable string from model
      * @return string, or <code>null</code>
      * @since 2.0
      */
     protected String resolveNLString(URL bundleURL, String string) {

         if (string == null)
             return null;

         String s = string.trim();

         if (s.equals("")) //$NON-NLS-1$
 return string;

         if (!s.startsWith(KEY_PREFIX))
             return string;

         if (s.startsWith(KEY_DOUBLE_PREFIX))
             return s.substring(1);

         int ix = s.indexOf(" "); //$NON-NLS-1$
 String key = ix == -1 ? s : s.substring(0, ix);
         String dflt = ix == -1 ? s : s.substring(ix + 1);

         ResourceBundle b = getResourceBundle(bundleURL);

         if (b == null)
             return dflt;

         try {
             return b.getString(key.substring(1));
         } catch (MissingResourceException e) {
             return dflt;
         }
     }

     /**
      * Returns a concrete array type for the elements of the specified
      * list. The method assumes all the elements of the list are the same
      * concrete type as the first element in the list.
      *
      * @param l list
      * @return concrete array type, or <code>null</code> if the array type
      * could not be determined (the list is <code>null</code> or empty)
      * @since 2.0
      */
     protected Object [] arrayTypeFor(List l) {
         if (l == null || l.size() == 0)
             return null;
         return (Object []) Array.newInstance(l.get(0).getClass(), 0);
     }

     /**
      * Returns a concrete array type for the elements of the specified
      * set. The method assumes all the elements of the set are the same
      * concrete type as the first element in the set.
      *
      * @param s set
      * @return concrete array type, or <code>null</code> if the array type
      * could not be determined (the set is <code>null</code> or empty)
      * @since 2.0
      */
     protected Object [] arrayTypeFor(Set s) {
         if (s == null || s.size() == 0)
             return null;
         Iterator i = s.iterator();
         return (Object []) Array.newInstance(i.next().getClass(), 0);
     }

     /**
         * Helper method to access resouce bundle for feature. The default
         * implementation attempts to load the appropriately localized
         * feature.properties file.
         *
         * @param url base URL used to load the resource bundle.
         * @return resource bundle, or <code>null</code>.
         * @since 2.0
         */
     protected ResourceBundle getResourceBundle(URL url) {

         if (url == null)
             return null;

         if (bundles == null) {
             bundles = new HashMap ();
         } else {
             ResourceBundle bundle = (ResourceBundle ) bundles.get(url.toExternalForm());
             if (bundle != null)
                 return bundle;
         }

         ResourceBundle bundle = null;
         try {
             url = UpdateManagerUtils.asDirectoryURL(url);
             ClassLoader l = new URLClassLoader (new URL [] { url }, null);
             bundle = ResourceBundle.getBundle(getPropertyName(), Locale.getDefault(), l);
             bundles.put(url.toExternalForm(), bundle);
         } catch (MissingResourceException e) {
             UpdateCore.warn(e.getLocalizedMessage() + ":" + url.toExternalForm()); //$NON-NLS-1$
 } catch (MalformedURLException e) {
             UpdateCore.warn(e.getLocalizedMessage());
         }
         return bundle;
     }

     /**
      * Method getPropertyName.
      * @return String
      */
     protected String getPropertyName() {
         return Feature.FEATURE_FILE;
     }

 }

